package com.hongji.lms5xxcommunicator.opc.client;

import com.hongji.lms5xxcommunicator.exception.OpcConnectionException;
import com.hongji.lms5xxcommunicator.utils.Utils;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig;
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;
import org.eclipse.milo.opcua.sdk.client.nodes.UaVariableNode;
import org.eclipse.milo.opcua.sdk.core.AccessLevel;
import org.eclipse.milo.opcua.stack.client.DiscoveryClient;
import org.eclipse.milo.opcua.stack.core.*;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.enumerated.UserTokenType;
import org.eclipse.milo.opcua.stack.core.types.structured.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@Slf4j
@Service
public class OpcUaClientServiceImpl implements OpcUaClientService {

    private static final long CONNECT_TIMEOUT_SEC = 50;
    private static final long DISCONNECT_TIMEOUT_SEC = 3;

    private final ConcurrentMap<String, OpcUaClient> clientCache = new ConcurrentHashMap<>();

    @Value("${opc.endpoint}")
    private String endpoint;

    @Value("${opc.request_timeout:5000}")
    private int requestTimeout;

    @Value("${opc.read_timeout:5}")
    private int readTimeout;

    @Value("${opc.write_timeout:5}")
    private int writeTimeout;

    public OpcUaClientServiceImpl() {
        log.info("OPC UA 客户端服务已初始化");
    }

    // ========================= API =========================

    @Override
    public boolean testConnection(String endpointUrl) throws OpcConnectionException {
        log.info("测试 OPC UA 连接，目标：{}", endpointUrl);
        OpcUaClient client = null;
        try {
            client = getOrCreateClient(endpointUrl, true);
            Object time = readNodeInternal(client, Identifiers.Server_ServerStatus_CurrentTime).getValue().getValue();
            log.info("连接成功，服务器时间：{}", time);
            return true;
        } catch (Exception e) {
            throw handleException("连接测试失败", e);
        } finally {
            safeDisconnect(client);
        }
    }

    @Override
    public Object readSingleNode(String nodeId) throws OpcConnectionException {
        log.info("读取节点（字符串）：{}", nodeId);
        NodeId nid = validateNodeId(nodeId);
        long start = System.currentTimeMillis();
        OpcUaClient client = null;
        try {
            client = getOrCreateClient(endpoint, false);
            Object val = readNodeInternal(client, nid).getValue().getValue();
            log.info("读取成功 [{}]，值={}，耗时={} ms", nid, val, System.currentTimeMillis() - start);
            return val;
        } catch (Exception e) {
            throw handleException("读取节点失败", e);
        }
    }

    @Override
    public Object readSingleNode(NodeId nodeId) throws OpcConnectionException {
        long start = System.currentTimeMillis();
        OpcUaClient client = null;
        try {
            client = getOrCreateClient(endpoint, false);
            Object val = readNodeInternal(client, nodeId).getValue().getValue();
            return val;
        } catch (Exception e) {
            throw handleException("读取节点失败", e);
        }
    }

    @Override
    public List<DataValue> readMultipleNodes(List<String> nodeIds) throws OpcConnectionException {
        long start = System.currentTimeMillis();
        OpcUaClient client = null;
        try {
            List<NodeId> parsed = (nodeIds == null ? Collections.emptyList() :
                    nodeIds.stream().map(this::validateNodeId).collect(Collectors.toList()));
            client = getOrCreateClient(endpoint, false);
            ReadResponse resp = executeRead(client, buildReadRequests(parsed));
            log.info("批量读取成功，共 {} 个节点，耗时={} ms", parsed.size(), System.currentTimeMillis() - start);
            return Arrays.asList(resp.getResults());
        } catch (Exception e) {
            throw handleException("批量读取失败", e);
        }
    }

    @Override
    public boolean writeSingleNode(String nodeId, Object value) throws OpcConnectionException {
        Objects.requireNonNull(value, "写入值不能为空");
        NodeId nid = validateNodeId(nodeId);
        long start = System.currentTimeMillis();
        OpcUaClient client = null;
        try {
            client = getOrCreateClient(endpoint, false);

            UaVariableNode var = getVariableNode(client, nid);
            checkWriteAccessLevel(var);
            checkDataTypeCompatibility(var, value);

            StatusCode sc = writeNodeInternal(client, nid, value);
            if (!sc.isGood()) throw new UaException(sc, "写入失败，状态码非 Good");

            log.info("写入成功 [{}]，值={}，耗时={} ms", nid, value, System.currentTimeMillis() - start);
            return true;
        } catch (Exception e) {
            throw handleException("写入节点失败", e);
        }
    }

    @Override
    public NodeId validateNodeId(String nodeId) {
        try {
            NodeId nid = Utils.parseFlexibleNodeId(nodeId); // 兼容 id= / s= / i= / nsu=
            log.debug("NodeId 解析成功：{} → {}", nodeId, nid);
            return nid;
        } catch (UaRuntimeException e) {
            throw new IllegalArgumentException("节点 ID 格式非法：" + nodeId, e);
        }
    }

    @PreDestroy
    public void shutdown() {
        log.info("应用关闭，清理 OPC UA 客户端连接");
        for (OpcUaClient c : clientCache.values()) {
            safeDisconnect(c);
        }
        clientCache.clear();
    }

    // ========================= 内部辅助 =========================

    private OpcUaClient getOrCreateClient(String endpointUrl, boolean forceNew) throws OpcConnectionException {
        if (!forceNew) {
            OpcUaClient cached = clientCache.get(endpointUrl);
            if (cached != null) return cached;
        }
        OpcUaClient client = createClient(endpointUrl);
        if (!forceNew) clientCache.put(endpointUrl, client);
        return client;
    }

    private OpcUaClient createClient(String endpointUrl) throws OpcConnectionException {
        try {
            EndpointDescription ep = discoverEndpoint(endpointUrl);
            OpcUaClient client = OpcUaClient.create(
                    OpcUaClientConfig.builder()
                            .setEndpoint(ep)
                            .setIdentityProvider(new AnonymousProvider())
                            .setRequestTimeout(Unsigned.uint(requestTimeout))
                            .build());
            client.connect().get(CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS);
            log.info("客户端创建并连接成功：{}", endpointUrl);
            return client;
        } catch (Exception e) {
            throw new OpcConnectionException("创建客户端失败", e);
        }
    }

    private EndpointDescription discoverEndpoint(String url) throws Exception {
        List<EndpointDescription> eps = DiscoveryClient.getEndpoints(url).get();
        Predicate<EndpointDescription> predicate = ep -> {
            if (!SecurityPolicy.None.getUri().equals(ep.getSecurityPolicyUri())) return false;
            UserTokenPolicy[] tokens = ep.getUserIdentityTokens();
            if (tokens == null) return false;
            for (UserTokenPolicy t : tokens) {
                if (t != null && t.getTokenType() == UserTokenType.Anonymous) return true;
            }
            return false;
        };
        for (EndpointDescription ep : eps) {
            if (predicate.test(ep)) return ep;
        }
        throw new OpcConnectionException("未找到支持匿名连接的端点");
    }

    private UaVariableNode getVariableNode(OpcUaClient client, NodeId nodeId) throws OpcConnectionException {
        try {
            return client.getAddressSpace().getVariableNode(nodeId);
        } catch (UaException e) {
            throw new OpcConnectionException("变量节点获取失败", e);
        }
    }

    private void checkWriteAccessLevel(UaVariableNode node) throws OpcConnectionException {
        int acc = node.getAccessLevel().intValue();
        int usr = node.getUserAccessLevel().intValue();
        boolean writable = (acc & AccessLevel.CurrentWrite.getValue()) != 0
                && (usr & AccessLevel.CurrentWrite.getValue()) != 0;
        if (!writable) throw new OpcConnectionException("该节点不允许写入");
    }

    private void checkDataTypeCompatibility(UaVariableNode node, Object value) throws OpcConnectionException {
        try {
            NodeId dt = node.getDataType();
            BuiltinDataType builtin = BuiltinDataType.fromNodeId(dt);
            if (builtin == null) { new Variant(value); return; }

            Class<?> expect = builtin.getBackingClass();
            Class<?> actual = value.getClass();

            if (expect == ByteString.class && (actual == byte[].class || actual == ByteString.class)) return;
            if (expect.isAssignableFrom(actual) || isNumericCompatible(expect, actual)) return;
            if (dt.equals(new NodeId(0, 29)) && (actual == Integer.class || actual == Long.class)) return;
            new Variant(value);
        } catch (Exception ex) {
            throw new OpcConnectionException("类型不兼容", ex);
        }
    }

    private boolean isNumericCompatible(Class<?> a, Class<?> b) {
        Set<Class<?>> nums = new HashSet<>(Arrays.asList(
                Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class,
                byte.class, short.class, int.class, long.class, float.class, double.class
        ));
        return nums.contains(a) && nums.contains(b);
    }

    private List<ReadValueId> buildReadRequests(List<NodeId> nodeIds) {
        List<ReadValueId> req = new ArrayList<>(nodeIds == null ? 0 : nodeIds.size());
        if (nodeIds != null) {
            for (NodeId id : nodeIds) {
                req.add(new ReadValueId(id, AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE));
            }
        }
        return req;
    }

    private ReadResponse executeRead(OpcUaClient client, List<ReadValueId> req) throws Exception {
        return client.read(0.0, TimestampsToReturn.Both, req)
                .get(readTimeout, TimeUnit.SECONDS);
    }

    private StatusCode writeNodeInternal(OpcUaClient client, NodeId nodeId, Object value) throws Exception {
        DataValue dv = new DataValue(new Variant(value), StatusCode.GOOD, null, DateTime.NULL_VALUE);
        WriteValue wv = new WriteValue(nodeId, AttributeId.Value.uid(), null, dv);
        WriteResponse resp = client.write(Collections.singletonList(wv))
                .get(writeTimeout, TimeUnit.SECONDS);
        return resp.getResults()[0];
    }

    private DataValue readNodeInternal(OpcUaClient client, String nodeIdStr) throws Exception {
        return client.readValue(0.0, TimestampsToReturn.Both, Utils.parseFlexibleNodeId(nodeIdStr))
                .get(readTimeout, TimeUnit.SECONDS);
    }

    private DataValue readNodeInternal(OpcUaClient client, NodeId nodeId) throws Exception {
        return client.readValue(0.0, TimestampsToReturn.Both, nodeId)
                .get(readTimeout, TimeUnit.SECONDS);
    }

    private void safeDisconnect(OpcUaClient client) {
        if (client != null) {
            try {
                client.disconnect().get(DISCONNECT_TIMEOUT_SEC, TimeUnit.SECONDS);
                log.info("客户端断开连接成功");
            } catch (Exception e) {
                log.warn("客户端断开连接失败：{}", e.getMessage());
            }
        }
    }

    private OpcConnectionException handleException(String msg, Exception e) {
        log.error("{}：{}", msg, e.toString());
        return new OpcConnectionException(msg, e);
    }

    /* ===== 新增：上下文打印（兼容 Milo 版本） ===== */
    public void dumpOpcContext() {
        try {
            OpcUaClient c = getOrCreateClient(endpoint, false);
            String url = (c.getConfig() != null && c.getConfig().getEndpoint() != null)
                    ? c.getConfig().getEndpoint().getEndpointUrl()
                    : "(unknown)";
            log.info("OPC Endpoint = {}", url);

            DataValue dv = c.readValue(0.0, TimestampsToReturn.Neither, Identifiers.Server_NamespaceArray)
                    .get(readTimeout, TimeUnit.SECONDS);
            Object v = dv.getValue() != null ? dv.getValue().getValue() : null;
            if (v instanceof String[]) {
                String[] arr = (String[]) v;
                for (int i = 0; i < arr.length; i++) log.info("Namespace[{}] = {}", i, arr[i]);
            } else if (v instanceof Object[]) {
                Object[] arr = (Object[]) v;
                for (int i = 0; i < arr.length; i++) log.info("Namespace[{}] = {}", i, arr[i]);
            } else {
                log.info("Server_NamespaceArray = {}", v);
            }
        } catch (Exception e) {
            log.warn("打印 OPC 上下文失败：{}", e.toString());
        }
    }
}
